// Sage Android Client.
// Connect to a Sage server, remotely execute commands and display results.
//
// Copyright (C) 2010, Harald Schilly <harald.schilly@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package org.sagemath.android.fx;

import org.sagemath.android.R;
import org.sagemath.android.Utils;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Edit a String to build up a function, like "1 + sin(x * cos(x)) / (1-x)".
 * This is an {@link Activity} and might also be used from other applications.
 * 
 * @author Harald Schilly
 */
final public class FunctionEditor extends Activity {
  
  private Button btnDone, btnCancel;
  
  private EditText formula;
  
  private TableLayout gridFunc, gridNavi, gridOps;
  
  final static Pattern wordBoundary = Pattern.compile("\\b");
  
  /**
   * Operators
   */
  final private static String[] staticOps = {
      "+", "-", "*", "/", "^", "x", "y", "z", "(", ")", "<", ">", ".", "[", "]", "<", ">"
  };
  
  /**
   * Entry point for creating the {@link FunctionEditor} Activity class.
   * 
   * @return intent with modified text, and the start and end position of the
   *         selection.
   */
  @Override
  final public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.function_editor);
    
    final LayoutInflater inflater = this.getLayoutInflater();
    
    // UI fields
    formula = (EditText) findViewById(R.id.fx_formula);
    btnDone = (Button) findViewById(R.id.fx_button_done);
    btnCancel = (Button) findViewById(R.id.fx_button_cancel);
    gridOps = (TableLayout) findViewById(R.id.fx_grid_ops);
    gridFunc = (TableLayout) findViewById(R.id.fx_grid_func);
    gridNavi = (TableLayout) findViewById(R.id.fx_grid_navi);
    
    {
      // Copy the text and selection state from the intent that started me
      Bundle bIntent = getIntent().getExtras();
      final String func = bIntent.getString("func");
      final int start = bIntent.getInt("start");
      final int end = bIntent.getInt("end");
      formula.setText(func);
      formula.setSelection(start, end);
    }
    
    // when done return the constructed string with the current selection in a
    // bundle
    btnDone.setOnClickListener(new OnClickListener() {
      @Override
      final public void onClick(View v) {
        Intent intent = new Intent();
        Bundle bundle = new Bundle();
        bundle.putString("func", formula.getText().toString());
        bundle.putInt("start", formula.getSelectionStart());
        bundle.putInt("end", formula.getSelectionEnd());
        intent.putExtra("data", bundle);
        setResult(RESULT_OK, intent);
        finish();
      }
    });
    
    // just signal that it was canceled
    btnCancel.setOnClickListener(new OnClickListener() {
      @Override
      final public void onClick(View v) {
        setResult(RESULT_CANCELED);
        finish();
      }
    });
    
    // The following sets the "keys" which insert something or additionally
    // deletes something if there is a selection
    {
      final int cols = (int) (Utils.getScaledWidth(this) / 60.0);
      int idx = 0;
      TableRow row = null;
      for (Key key : Key.values()) {
        if (idx % cols == 0) {
          row = new TableRow(this);
          gridFunc.addView(row);
        }
        idx++;
        KeyView cell = (KeyView) inflater.inflate(R.layout.function_editor_key, null);
        cell.setText(key.toString());
        cell.setKey(key);
        cell.setOnClickListener(keyClickListener);
        row.addView(cell);
      }
    }
    
    // this part inserts operators like +, -, ...
    {
      final int cols = (int) (Utils.getScaledWidth(this) / 40.0);
      TableRow row = null;
      int idx = 0;
      for (String op : staticOps) {
        if (idx % cols == 0) {
          row = new TableRow(this);
          gridOps.addView(row);
        }
        idx++;
        TextView cell = (TextView) inflater.inflate(R.layout.function_editor_textview, null);
        cell.setText(op);
        cell.setOnClickListener(opClickListener);
        row.addView(cell);
      }
    }
    
    // this part is lengthy and does some actions like cursor move, selections
    // and deletions
    {
      final int cols = (int) (Utils.getScaledWidth(this) / 40.0);
      TableRow row = null;
      int idx = 0;
      for (Action act : Action.values()) {
        if (idx % cols == 0) {
          row = new TableRow(this);
          gridNavi.addView(row);
        }
        idx++;
        ActionView cell = (ActionView) inflater.inflate(R.layout.function_editor_action, null);
        cell.setText(act.toString());
        cell.setAction(act);
        cell.setOnClickListener(actionClickListener);
        row.addView(cell);
      }
    }
  }
  
  final private OnClickListener opClickListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
      final String op = (String) ((TextView) v).getText();
      final int start = formula.getSelectionStart();
      final int end = formula.getSelectionEnd();
      Editable formulaEdit = formula.getEditableText();
      formulaEdit.replace(start, end, " " + op + " ");
      formula.setText(formulaEdit.toString());
      formula.setSelection(start + op.length() + 2);
    }
  };
  
  /**
   * This is the action listener, avoiding to create one for each key.
   */
  final private OnClickListener keyClickListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
      final Key f = ((KeyView) v).getKey();
      final int oldPos = formula.getSelectionStart();
      String s = formula.getText().replace(oldPos, formula.getSelectionEnd(), f.getInsert())
          .toString();
      formula.setText(s);
      formula.setSelection(oldPos + f.getOffset());
    }
  };
  
  /**
   * This is the action listener, avoiding to create one for each action.
   */
  final private OnClickListener actionClickListener = new OnClickListener() {
    @Override
    public void onClick(View v) {
      final Action curAct = ((ActionView) v).getAction();
      final int len = formula.length();
      final int selLeft = formula.getSelectionStart();
      final int selRght = formula.getSelectionEnd();
      final int oneLeft = (selLeft <= 0) ? selLeft : selLeft - 1;
      final int oneRght = (selRght >= len - 1) ? selRght : selRght + 1;
      // ~~~ one word left and right
      final int oneWordRight, oneWordLeft;
      { // TODO make this correct
        final CharSequence leftpart = formula.getText().subSequence(0, selLeft);
        final CharSequence rightpart = formula.getText().subSequence(selRght, len - 1);
        Matcher matcher = wordBoundary.matcher(rightpart);
        int onerightmatchtmp;
        try {
          onerightmatchtmp = matcher.end();
        } catch (IllegalStateException ise) {
          onerightmatchtmp = oneRght;
        }
        final int onerightmatch = onerightmatchtmp;
        final Matcher leftmatch = wordBoundary.matcher(leftpart);
        oneWordRight = (onerightmatch >= len - 1) ? len - 1 : onerightmatch;
        int oneleftmatchtmp;
        try {
          oneleftmatchtmp = leftmatch.start(leftmatch.groupCount() - 1);
        } catch (IllegalStateException ise) {
          oneleftmatchtmp = oneLeft;
        }
        final int oneleftmatch = oneleftmatchtmp;
        oneWordLeft = (oneleftmatch >= 0) ? oneleftmatch : 0;
      }
      // ~~~ end of one word left and right
      
      final Editable formulaEdit = formula.getEditableText();
      
      switch (curAct) {
        
        case Linebreak:
          // TODO get the system specific linebreak
          formulaEdit.insert(selRght, "\n");
          break;
        
        case Del:
          if (selLeft == selRght) {
            formulaEdit.delete(selLeft, oneRght);
          } else {
            formulaEdit.delete(selLeft, selRght);
          }
          break;
        
        case DelLeftOneChar:
          formulaEdit.delete(oneLeft, selRght);
          break;
        
        case DelRightOneChar:
          formulaEdit.delete(selLeft, oneRght);
          break;
        
        case SelLeftOneChar:
          formula.setSelection(oneLeft, selRght);
          break;
        
        case SelRightOneChar:
          formula.setSelection(selLeft, oneRght);
          break;
        
        case SelLeftOneWord:
          formula.setSelection(oneWordLeft, selRght);
          break;
        
        case SelRightOneWord:
          formula.setSelection(selLeft, oneWordRight);
          break;
        
        default:
          String msg = "ERROR: undefined action " + curAct.toString();
          Toast.makeText(FunctionEditor.this, msg, Toast.LENGTH_SHORT).show();
      }
      
      formula.setText(formulaEdit.toString());
    }
  };
}

/**
 * utf8 arrows for navigation and actions like deleting.
 * <a href="http://en.wikipedia.org/wiki/Arrow_%28symbol%29">Arrows at
 * Wikipedia</a>
 */
enum Action {
  // "\u00b6" // pilcrow sign
  Linebreak("\u21b2"), // down left for linebreak
  Del("\u21af"), // flash light arrow
  LeftMoveWordBoundary("\u219e"), // left two heads
  RightMoveWordBoundary("\u21a0"), // right two heads
  ChangeCase("\u21c5"), // up down double
  DelLeftOneChar("\u21dc"), // left wriggle
  DelLeftRightWordBoundary("\u21ad"), // left right wriggle
  DelRightOneChar("\u21dd"), // right wriggle
  SelLeftOneChar("\u21e6"), // left thick
  SelRightOneChar("\u21e8"), // right thick
  SelLeftOneWord("\u21da"), // left triple
  SelRightOneWord("\u21db"); // right triple
  
  final private String sign;
  
  Action(String sign) {
    this.sign = sign;
  }
  
  @Override
  public String toString() {
    return sign;
  }
}

/**
 * Defines the special keys to insert functions. <br>
 * The argument <code>insert</code> might contain the special character "|"
 * (pipe symbol) which stands for the insert sign. Otherwise, just specify the
 * <code>offset</code> parameter.
 */
enum Key {
  // zero("0"), one("1"), two("2"), three("3"), four("4"), //
  // five("5"), six("6"), seven("7"), eight("8"), nine("9"), //
  sin("sin"), cos("cos"), Tan("tan"), // trig
  asin("asin"), acos("acos"), atan("atan"), // trig inv
  infinity("\u221e", "oo"), // special
  integrate("\u222b", "integrate(|, x)"), diff("dx", "diff(|, x)"), // calc
  limit("limit", "limit(|, x = 0)");
  
  final private String display, insert;
  
  final private int offset;
  
  Key(String display, boolean append) {
    this(display, append ? display + "(|)" : display);
  }
  
  Key(String display) {
    this(display, true);
  }
  
  Key(String display, String insert) {
    this(display, insert, 0);
  }
  
  Key(String display, String insert, int offset) {
    this.display = display;
    this.insert = insert;
    this.offset = offset;
  }
  
  /**
   * @return the relative position where the cursor should be placed from the
   *         current position.
   *         i.e. if we have the string AB|CD with the cursor between B and C,
   *         and we insert XY with an offset of 2, we should end up with
   *         ABXY|CD.
   */
  final int getOffset() {
    if (offset > 0) {
      return offset;
    }
    int idx = insert.indexOf("|");
    if (idx >= 0) {
      return idx;
    }
    return insert.length();
  }
  
  /**
   * @return the string that should actually be inserted
   */
  final String getInsert() {
    return insert.replaceAll("\\|", "");
  }
  
  @Override
  final public String toString() {
    return display;
  }
}
